/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 *
 * ident	"%Z%%M%	%I%	%E% SMI"
 */
package org.opensolaris.os.dtrace;

import java.util.*;
import java.io.*;
import java.beans.*;

/**
 * A value generated by the DTrace {@code ustack()} or {@code jstack()}
 * action.
 * <p>
 * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
 *
 * @author Tom Erickson
 */
public final class UserStackRecord implements StackValueRecord,
       Serializable, Comparable <UserStackRecord>
{
    static final long serialVersionUID = -4195269026915862308L;

    static {
	try {
	    BeanInfo info = Introspector.getBeanInfo(UserStackRecord.class);
	    PersistenceDelegate persistenceDelegate =
		    new DefaultPersistenceDelegate(
		    new String[] {"processID", "stackFrames", "rawStackData"})
	    {
		/*
		 * Need to prevent DefaultPersistenceDelegate from using
		 * overridden equals() method, resulting in a
		 * StackOverFlowError.  Revert to PersistenceDelegate
		 * implementation.  See
		 * http://forum.java.sun.com/thread.jspa?threadID=
		 * 477019&tstart=135
		 */
		protected boolean
		mutatesTo(Object oldInstance, Object newInstance)
		{
		    return (newInstance != null && oldInstance != null &&
			    oldInstance.getClass() == newInstance.getClass());
		}
	    };
	    BeanDescriptor d = info.getBeanDescriptor();
	    d.setValue("persistenceDelegate", persistenceDelegate);
	} catch (IntrospectionException e) {
	    System.out.println(e);
	}
    }

    private transient KernelStackRecord stackRecord;
    /** @serial */
    private final int processID;

    /**
     * Called by native code.
     */
    private
    UserStackRecord(int pid, byte[] rawBytes)
    {
	stackRecord = new KernelStackRecord(rawBytes);
	processID = pid;
	validate();
    }

    /**
     * Creates a {@code UserStackRecord} with the given stack frames,
     * user process ID, and raw stack data.  Supports XML persistence.
     *
     * @param frames array of human-readable stack frames, copied so
     * that later modifying the given frames array will not affect this
     * {@code UserStackRecord}; may be {@code null} or empty to indicate
     * that the raw stack data was not converted to human-readable stack
     * frames (see {@link StackValueRecord#getStackFrames()})
     * @param pid non-negative user process ID
     * @param rawBytes array of raw bytes used to represent this stack
     * value in the native DTrace library, needed to distinguish stacks
     * that have the same display value but are considered distinct by
     * DTrace; copied so that later modifying the given array will not
     * affect this {@code UserStackRecord}
     * @throws NullPointerException if the given array of raw bytes is
     * {@code null} or if any element of the {@code frames} array is
     * {@code null}
     * @throws IllegalArgumentException if the given process ID is
     * negative
     */
    public
    UserStackRecord(int pid, StackFrame[] frames, byte[] rawBytes)
    {
	stackRecord = new KernelStackRecord(frames, rawBytes);
	processID = pid;
	validate();
    }

    private final void
    validate()
    {
	if (processID < 0) {
	    throw new IllegalArgumentException("process ID is negative");
	}
    }

    public StackFrame[]
    getStackFrames()
    {
	return stackRecord.getStackFrames();
    }

    void
    setStackFrames(StackFrame[] frames)
    {
	stackRecord.setStackFrames(frames);
    }

    /**
     * Gets the native DTrace representation of this record's stack as
     * an array of raw bytes.  The raw bytes include the process ID and
     * are used in {@link #equals(Object o) equals()} and {@link
     * #compareTo(UserStackRecord r) compareTo()} to test equality and
     * to determine the natural ordering of user stack records.
     *
     * @return the native DTrace library's internal representation of
     * this record's stack as a non-null array of bytes
     */
    public byte[]
    getRawStackData()
    {
	return stackRecord.getRawStackData();
    }

    /**
     * Gets the raw bytes used to represent this record's stack value in
     * the native DTrace library.  To get a human-readable
     * representation, call {@link #toString()}.
     *
     * @return {@link #getRawStackData()}
     */
    public Object
    getValue()
    {
	return stackRecord.getValue();
    }

    /**
     * Gets the process ID associated with this record's user stack.
     *
     * @return non-negative pid
     */
    public int
    getProcessID()
    {
	return processID;
    }

    public List <StackFrame>
    asList()
    {
	return stackRecord.asList();
    }

    /**
     * Gets a {@code KernelStackRecord} view of this record.
     *
     * @return non-null {@code KernelStackRecord} view of this record
     */
    public KernelStackRecord
    asKernelStackRecord()
    {
	return stackRecord;
    }

    /**
     * Compares the specified object with this {@code UserStackRecord}
     * for equality.  Returns {@code true} if and only if the specified
     * object is also a {@code UserStackRecord} and both stacks have the
     * same raw stack data (including process ID).
     * <p>
     * This implementation first checks if the specified object is this
     * {@code UserStackRecord}.  If so, it returns {@code true}.
     *
     * @return {@code true} if and only if the specified object is also
     * a {@code UserStackRecord} and both stacks have the same raw stack
     * data (including process ID)
     */
    @Override
    public boolean
    equals(Object o)
    {
	if (o == this) {
	    return true;
	}
	if (o instanceof UserStackRecord) {
	    UserStackRecord r = (UserStackRecord)o;
	    return stackRecord.equals(r.stackRecord);
	}
	return false;
    }

    /**
     * Overridden to ensure that equal instances have equal hash codes.
     */
    @Override
    public int
    hashCode()
    {
	return stackRecord.hashCode();
    }

    /**
     * Compares this record with the given {@code UserStackRecord}.
     * Compares the first unequal pair of bytes at the same index in
     * each record's raw stack data, or if all corresponding bytes are
     * equal, compares the length of each record's array of raw stack
     * data.  Corresponding bytes are compared as unsigned values.  The
     * {@code compareTo()} method is compatible with {@link
     * #equals(Object o) equals()}.
     * <p>
     * This implementation first checks if the specified object is this
     * {@code UserStackRecord}.  If so, it returns {@code 0}.
     *
     * @return -1, 0, or 1 as this record's raw stack data is less than,
     * equal to, or greater than the given record's raw stack data
     */
    public int
    compareTo(UserStackRecord r)
    {
	if (r == this) {
	    return 0;
	}

	return stackRecord.compareTo(r.stackRecord);
    }

    /**
     * Serialize this {@code UserStackRecord} instance.
     *
     * @serialData Serialized fields are emitted, followed first by this
     * record's stack frames as an array of type {@link String}, then by
     * this record's raw stack data as an array of bytes.
     */
    private void
    writeObject(ObjectOutputStream s) throws IOException
    {
	s.defaultWriteObject();
	s.writeObject(stackRecord.getStackFrames());
	s.writeObject(stackRecord.getRawStackData());
    }

    private void
    readObject(ObjectInputStream s) throws IOException, ClassNotFoundException
    {
	s.defaultReadObject();
	try {
	    StackFrame[] frames = (StackFrame[])s.readObject();
	    byte[] rawBytes = (byte[])s.readObject();
	    // defensively copies stack frames and raw bytes
	    stackRecord = new KernelStackRecord(frames, rawBytes);
	} catch (Exception e) {
	    InvalidObjectException x = new InvalidObjectException(
		    e.getMessage());
	    x.initCause(e);
	    throw x;
	}
	// check class invariants
	try {
	    validate();
	} catch (Exception e) {
	    InvalidObjectException x = new InvalidObjectException(
		    e.getMessage());
	    x.initCause(e);
	    throw x;
	}
    }

    /**
     * Gets the {@link KernelStackRecord#toString() string
     * representation} of the view returned by {@link
     * #asKernelStackRecord()} preceded by the user process ID on its
     * own line.  The process ID is in the format {@code process ID:
     * pid} (where <i>pid</i> is a decimal integer) and is indented by
     * the same number of spaces as the stack frames.  The exact format
     * is subject to change.
     */
    public String
    toString()
    {
	StringBuilder buf = new StringBuilder();
	final int stackindent = KernelStackRecord.STACK_INDENT;
	int i;
	buf.append('\n');
	for (i = 0; i < KernelStackRecord.STACK_INDENT; ++i) {
	    buf.append(' ');
	}
	buf.append("process ID: ");
	buf.append(processID);
	buf.append(stackRecord.toString()); // starts with newline
	return buf.toString();
    }
}
